package chair;

import java.util.ArrayList;

// FIXME: utilise chair.Queue tant que pas interface
//import java.util.Queue;

import java.util.Random;

import chair.Reflex.ReflexType;
import chair.core.Diary;
import chair.core.Stimulus;

/**
 * Classe destinée à contenir à la fois les réflexes produits par organe et les
 * comportements que peut effectuer le robot en situation normale. Le but est de
 * pouvoir gérer le conflit lorsque des comportements et des réflexes utilisent
 * les mêmes parties du robot.
 * 
 * Un réflexe aura toujours priorité sur un comportement. Réflexe : premier
 * arrivé premier servi Comportement : le plus prometteur sélectionné sinon au
 * hasard
 * 
 * Attention pour une même fonction chaque réflexe ou comportement peut s'il le
 * désire éteindre son action lorsque sera appelé run(false). Dès qu'il n'aura
 * plus la main les outils qu'ils controlait seront laissés à la pleine
 * disposition de son successeur. D'où l'importance de bien définir les parentés
 * via Function.
 * 
 * @author nomhad
 * 
 */
final public class Function {

	/**
	 * 
	 * Une fonction a besoin de connaître trois chose pour chaque réflexe
	 * qu'elle doit gérer : ce qu'il est, l'ordre à lui donner, et le nom de la
	 * sensation associée. Centralise ces trois éléments. Permet d'éviter de
	 * faire trois files différentes. Une chose monstrueuse que JAMAIS je ne
	 * ferais.
	 * 
	 */
	private class ReflexCommand {
		/** réflexe à engager */
		Reflex reflex;
		/** commande à lui donner */
		Impulse impulse;

		ReflexCommand(Reflex ref, Impulse imp) {
			reflex = ref;
			impulse = imp;
		}

		/**
		 * Va envoyer au réflexe son impulsion et va dereferencer
		 * Function._currentReflexCommand si c'était une extinction.
		 */
		void fire() {
			reflex.fire(impulse);
			if (!impulse.getToggle())
				_currentReflexCommand = null;
		}

		/**
		 * Utilisé dans le cas où on n'attend pas de front descendant, modifie
		 * impulsion pour éteindre soi-même le réflexe.
		 */
		void extinct() {
			// Change d'impulsion avant d'envoyer ordre.
			// Vraiment utile de cloner ?
			impulse = (Impulse) impulse.clone();
			impulse.setToggle(false);
			fire();
		}
	}

	/**
	 * Mon petit générateur de nombre aléatoire utilisé dans tick(). Un nom tout
	 * à fait banal et explicite ? Je me ramolis...
	 */
	private static Random random = new Random();
	/**
	 * Liste des comportements que le robot peut adopter
	 * 
	 * TODO: pas meilleur structure de donnée, faudrait un tri automatique
	 * (limitation leJOS)
	 */
	private ArrayList<Behavior> _listBehaviors = new ArrayList<Behavior>();
	/** File des commandes qu'il restes à exécuter concernant les réflexes */
	private Queue _queueReflexeCommands = new Queue();
	/** Nom associé à la fonction */
	private String _name = "Function";
	/**
	 * Le reflex et sa commande actuellement en cours. null si aucun. Attention,
	 * quand on il donne l'ordre d'éteindre un réflexe, il va réinitialiser tout
	 * seul cette variable à null
	 */
	private ReflexCommand _currentReflexCommand = null;
	/**
	 * Comportement actuel : on est quand même gentil, on lui dit de s'arrêter
	 * avant de passer à autre chose
	 */
	private Behavior _currentBehavior = null;
	/** À quel moment le comportement actuel a-t-il commencé ? */
	private long _currentBehaviorTimeStarted = -1;
	/**
	 * Utilisé dans addResponse et addReflexe : est-ce que les réflexe ont une
	 * cause réelle et non prédictive ?
	 */
	private boolean _realReaction = false;
	/** Le cerveau qui chapeaute le tout */
	private Brain _brain;

	/**
	 * S'enregistre auprès de Striatum
	 * 
	 * @param brain
	 *            le cerveau auquel est relié cette fonction
	 * @param name
	 *            nom de la fonction
	 */
	public Function(Brain brain, String name) {
		_name = name;
		_brain = brain;
		brain.addFunction(this);
	}

	/**
	 * Est-ce qu'il y a des réflexes dans la file ?
	 * 
	 * @return true si réflexe dans file, fales sinon
	 */
	private boolean isReflexCommandQueueEmpty() {
		synchronized (_queueReflexeCommands) {
			// TODO: Faire une structure pour les deux listes ?
			return _queueReflexeCommands.isEmpty();
		}
	}

	/**
	 * Accesseur pour le nom de la fonction. Utilisé par Brain.plug(Organ,
	 * Function) pour constuire nom du réflexe LOCK
	 * 
	 * @return nom de la fonction
	 */
	String getName() {
		return _name;
	}

	/**
	 * Permet à addReflex et addResponse de signaler type de réflexe en cours
	 * dans fonction
	 * 
	 * @param toggle
	 *            nouvelle valeur de _realReaction
	 */
	private void setRealReaction(boolean toggle) {
		_realReaction = toggle;
	}

	/**
	 * Si les réflexes en cours ou dans file sont une réponse physiologique et
	 * non prédictive alors on ajoutera pas de prédictions
	 * 
	 * @param reflex
	 *            Reflex (réponses) à ajouter dans la file
	 * @param impulse
	 *            impulsion à envoyer à ce réflexe
	 */
	synchronized void addResponse(Reflex reflex, Impulse impulse) {
		if (!_realReaction) {
			// Place libre : on ajoute
			addReflex(reflex, impulse);
			// Oublie pas de signaler que c'est à une prédiction qu'on a ici
			// affaire
			setRealReaction(false);
		}
	}

	/**
	 * On ajoute dans la file à la fois le réflexe en question et l'ordre
	 * associé (l'allumer ou l'éteindre)
	 * 
	 * @param reflex
	 *            Reflex à ajouter dans la file
	 * @param impulse
	 *            impulsion à envoyer à ce réflexe
	 */
	void addReflex(Reflex reflex, Impulse impulse) {
		ReflexCommand refCom = new ReflexCommand(reflex, impulse);
		synchronized (_queueReflexeCommands) {
			_queueReflexeCommands.push(refCom);
			// À ce niveau on pense que ce sont de vrais réflexes
			setRealReaction(true);
		}
	}

	/**
	 * On ajoute un comportement dans la liste
	 * 
	 * @param behavior
	 *            Behavior à ajouter
	 */
	void addBehavior(Behavior behavior) {
		synchronized (_listBehaviors) {
			_listBehaviors.add(behavior);
		}
	}

	/**
	 * Retourne le prochain comportement à adopter. Va en choisir un au hasard
	 * parmi ceux prédisant la meilleure récompense. Utilisé dans tick(). Ne va
	 * observer que le front montant dans le cas d'un comportement spontané.
	 * 
	 * Si aucun comportement ne peut être sélectionné (tous se déclare comme
	 * n'étant plus d'actualité) renvoie -1.
	 * 
	 * @return index correspondant de _listBehaviors
	 */
	private Behavior selectNextBehavior() {
		synchronized (_listBehaviors) {
			// Le nombre de comportements au total
			int nbBev = _listBehaviors.size();
			// Erreur de casting : rien à sélectionner
			if (nbBev == 0)
				return null;
			// Un seul comportement pertinent : inutile de se prendre la tête
			else if (nbBev == 1 && _listBehaviors.get(0).isRelevant())
				return _listBehaviors.get(0);
			// Nombre de comportement qu'on peut sélectionner
			int n = 0;
			// Histoire de ne pas créer de problème de synchronisation si depuis
			// l'extérieur on modifie Behavior._relevant, il va garder ceux qui
			// sont d'actualité en une passe.
			Behavior[] tabBev = new Behavior[nbBev];
			for (int i = 0; i < nbBev; i++) {
				Behavior bev = _listBehaviors.get(i);
				// Attention, seules les premières cases sont remplies
				if (bev.isRelevant())
					tabBev[n++] = bev;
			}
			// Plus rien à voir, circulez
			if (n == 0)
				return null;
			// Un seul comportement pertinent : inutile de faire plus de calculs
			if (n == 1)
				return tabBev[0];

			// Un tableau contenant la récompense prédite pour chaque
			// comportement
			int[] rewards = new int[n];
			// Ici les premières cases stockent les indicent des plus hautes
			// récompenses
			int[] maxRewards = new int[n];
			// La plus haute récompense rencontrée
			int maxReward = 0;
			// Le nombre de fois où elle apparaît
			int nbMaxReward = 0;
			// Pour chaque comportement pertinent
			for (int i = 0; i < n; i++) {
				Behavior bev = tabBev[i];
				// Initialise récompense associé
				rewards[i] = 0;
				// Regarde ce que prédit le modèle si un stimulus est associé.
				// Vérifie seulement le front montant du stimulus, ajoute
				// récompense à rewards[]
				Stimulus bevStim = bev.getStimulus();
				if (bevStim != null) {
					for (Stimulus stim : _brain.checkForecastProbability(
							bevStim, true)) {
						rewards[i] += stim.getReward();
					}
					// front descendant vérifié seulement si comportement non
					// spontané
					if (!bev.isSpontaneous())
						for (Stimulus stim : _brain.checkForecastProbability(
								bevStim, false)) {
							rewards[i] += stim.getReward();
						}
					Diary.logln("Récompense associée à " + bev + " : "
							+ rewards[i]);
				}
				// Initialise maxReward, MAJ nbMaxReward et maxRewards[]
				if (i == 0) {
					maxReward = rewards[0];
					maxRewards[0] = i;
					nbMaxReward = 1;
				}
				// Met à jour aux itérations suivantes
				else {
					// Trouvé plus grand récompense : on repart pour un tour
					if (rewards[i] > maxReward) {
						maxReward = rewards[i];
						maxRewards[0] = i;
						nbMaxReward = 1;
					} else
					// Une autre récompense qu'on ajoute à la liste des VIP
					if (rewards[i] == maxReward)
						maxRewards[nbMaxReward++] = i;
				}
			}
			// On a plus qu'à tirer au sort parmi le nombre d'élus et retourner
			// son p'tit numéro
			int k = random.nextInt(nbMaxReward);
			return tabBev[maxRewards[k]];
		}
	}

	/**
	 * 
	 * Arrête ou active le comportement actuel s'il existe
	 * 
	 * @param toggle
	 *            toujours entendre la même chanson : false éteindre, true
	 *            ignition
	 */
	private void toggleCurrentBehavior(boolean toggle) {
		if (_currentBehavior != null) {
			// Envoie au modèle stimulus si existe
			Stimulus stim = _currentBehavior.getStimulus();
			if (stim != null) {
				// N'envoie un front descendant que si le comportement n'est pas
				// déclaré comme spontanné.
				if (toggle)
					_brain.handleEvent(stim, toggle);
				else if (!_currentBehavior.isSpontaneous())
					_brain.handleEvent(stim, toggle);
			}
			// Ordre correspondant à envoyer au comportement
			_currentBehavior.run(toggle);
			// Dans le cas de l'extinction un peu de réinitialisation
			if (!toggle) {
				_currentBehavior = null;
				_currentBehaviorTimeStarted = -1;
			}
			// Sinon...pas classe mais géré dans tick()
		}
	}

	/**
	 * Active les réflexe de la file ou comportement si plus d'activté réflexe
	 * 
	 */
	void tick() {
		// On regarde le temps qu'il est
		long time = Chronos.top();

		if (_currentReflexCommand != null) {
			Reflex currentReflex = _currentReflexCommand.reflex;
			if (currentReflex.getType() == ReflexType.LOCK) {
				// Le réflexe courant est de type LOCK, il faudra attendre que
				// l'organe le possédant le libère de lui-même avant d'utliser à
				// nouveau la fonction
				if (currentReflex.isLocked()) {
					return;
				}
				// a été libéré, go-on !
				else {
					_currentReflexCommand.extinct();
				}
			}

			// Le réflexe courant a un timer : possible que monopolise
			// encore Function
			if (currentReflex.hasTimer()) {
				// Réflexe courant doit se terminer
				if (currentReflex.isTimeUp(time)) {
					_currentReflexCommand.extinct();
					_currentReflexCommand = null;
				}
				// Sinon on peut encore attendre un tour avant que cette
				// fonction s'essaye à une autre activité
				else
					return;
			}
		}

		synchronized (_queueReflexeCommands) {
			// Des reflexes en attente d'être lancé : go
			if (!isReflexCommandQueueEmpty()) {
				// Chez nous on se fait pas de claquage musculaire à caues
				// d'un mauvaise réflexe (...hum... "FIXEZMOI" ?)
				toggleCurrentBehavior(false);
				// défile réflexe et commande correspondante
				_currentReflexCommand = (ReflexCommand) _queueReflexeCommands
						.pop();
				// _currentReflexSensingName = refCom.sensingName;
				_currentReflexCommand.fire();
			}
		}

		// Est-ce que cette fonction a des comportements associés ?
		boolean hasBehaviors = false;
		synchronized (_listBehaviors) {
			hasBehaviors = _listBehaviors.size() != 0;
		}

		// Si plus de réflexe dans file et aucun réflex en cours on
		// sélectionne comportement
		if (isReflexCommandQueueEmpty() && _currentReflexCommand == null
				&& hasBehaviors) {

			// Peut accepter à nouveau des réflexes prédictifs
			setRealReaction(false);
			// Première chose à faire quand on deal avec les
			// comportement : tocker à leur porte.
			if (_currentBehavior != null)
				_currentBehavior.tick(time);

			// Choisi nouveau comportement via selectNextBehavior()
			// si aucun en cours ou tous les _pollingBehaviorTime...
			// pour peu que le comportement actuel nous permette de
			// l'arrêter
			if (_currentBehavior == null
					|| (time - _currentBehaviorTimeStarted > Chronos
							.getBehaviorPollingTime()
							&& _currentBehavior != null && _currentBehavior
							.isStoppable())) {
				toggleCurrentBehavior(false);
				Behavior nextBev = selectNextBehavior();
				// Aucun comportement n'a pu être sélectionné
				if (nextBev == null) {
					Diary.logln(_name + "Aucun comportement qu'on puisse "
							+ "actuellement sélectionner");
				}
				// Sinon on lance le nouveau
				else {
					_currentBehavior = nextBev;
					Diary.logln("Viens de sélectionner comportement "
							+ _currentBehavior._name);
					toggleCurrentBehavior(true);
					_currentBehaviorTimeStarted = time;
				}
			}
		}
	}

	@Override
	public String toString() {
		return "Function [_name=" + _name + "]";
	}
}
